JS基础:Event Loop

从一道笔试题开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}

console.log('script start');

setTimeout(function() {
console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

/* 输出结果
* script start
* async1 start
* async2
* promise1
* script end
* async1 end
* promise2
* setTimeout
**/

执行栈与任务队列

  • JS分为同步任务和异步任务。
  • 同步任务都在主线程上执行,形成一个执行栈。
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
执行栈

可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。

当我们调用一个方法的时候,JavaScript 会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。

由于 JavaScript 是单线程的,这些方法就会按顺序被排列在一个单独的地方,这个地方就是所谓执行栈。

任务队列

事件队列是一个存储着 异步任务 的队列,其中的任务严格按照时间先后顺序执行。

宏任务与微任务

  • 异步任务分为 宏任务(macrotask) 与 微任务 (microtask)。
  • 宏任务会进入一个队列,而微任务会进入到另一个不同的队列,且微任务要优于宏任务执行。
  • 一个宏任务在执行的过程中添加一些微任务,在当前的微任务没有执行完成时,不会执行下一个宏任务的
宏任务

宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering

微任务

微任务包括 process.nextTick ,promise ,MutationObserver,其中 process.nextTick 为 Node 独有。

笔试题思路

await

从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。

await是一个让出线程的标志。await之前的代码同步执行,由于async await本身就是promise+generator的语法糖,所以await后面的代码是microtask。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async function async1() {
console.log('async1 start'); // 2
await async2();
console.log('async1 end'); //6
}
async function async2() {
console.log('async2'); // 3
}

console.log('script start'); // 1

setTimeout(function() {
console.log('setTimeout'); // 8
}, 0)

async1();

new Promise(function(resolve) {
console.log('promise1'); // 4
resolve();
}).then(function() {
console.log('promise2'); //7
});
console.log('script end'); // 5

两道练习题

第一题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const p1 = new Promise((resolve, reject) => {
console.log('promise1');
resolve();
})
.then(() => {
console.log('then11');
new Promise((resolve, reject) => {
console.log('promise2');
resolve();
})
.then(() => {
console.log('then21');
})
.then(() => {
console.log('then23');
});
})
.then(() => {
console.log('then12');
});

const p2 = new Promise((resolve, reject) => {
console.log('promise3');
resolve();
}).then(() => {
console.log('then31');
});

/*
** promise1
* promise3
* then11
* promise2
* then31
* then21
* then12
* then23
**/
第二题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const p1 = new Promise((resolve, reject) => {
console.log('promise1'); // 1
resolve();
})
.then(() => {
console.log('then11'); // 2
return new Promise((resolve, reject) => {
console.log('promise2'); // 3
resolve();
})
.then(() => {
console.log('then21'); // 4
})
.then(() => {
console.log('then23'); // 5
});
})
.then(() => {
console.log('then12'); //6
});

注意两道题区别,p1 then() 里面,第一题是 new 一个 Promsie,第二题是 return 一个 Promise。new 的 Promise then回调会进入任务队列,而 return 的 Promise 其结果会被 resolve,所以第 18 行的 then 回调只有等这个Promise全部执行完再执行。

参考资料

第 10 题:常见异步笔试题

从一道题浅说 JavaScript 的事件循环

最后一次搞懂 EventLoop

微任务、宏任务与Event-Loop

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理